package com.chang.seckill.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.chang.common.to.mq.SeckillOrderTo;
import com.chang.common.utils.R;
import com.chang.common.utils.Result;
import com.chang.common.vo.MemberResponseVo;
import com.chang.seckill.feign.CouponFeignService;
import com.chang.seckill.feign.ProductFeignService;
import com.chang.seckill.interceptor.LoginUserInterceptor;
import com.chang.seckill.service.SeckillService;
import com.chang.seckill.to.SeckillSkuRedisTo;
import com.chang.seckill.vo.SeckillSessionWithSkus;
import com.chang.seckill.vo.SeckillSkuVo;
import com.chang.seckill.vo.SkuInfoVo;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RSemaphore;
import org.redisson.api.RedissonClient;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * @author: changzhenxi
 * @date: 2025年04月09日 16:57
 * @Version: 1.0
 * @description:
 */
@Slf4j
@Service
public class SeckillServiceImpl implements SeckillService {

    @Resource
    CouponFeignService couponFeignService;
    @Resource
    StringRedisTemplate stringRedisTemplate;

    @Resource
    RabbitTemplate rabbitTemplate;

    @Resource
    ProductFeignService productFeignService;

    @Resource
    RedissonClient redissonClient;

    private final String SESSION_CACHE_PREFIX = "seckill:session:";
    private final String SKU_CACHE_PREFIX = "seckill:skus";
    private final String SKU_STOCK_SEMAPHORE = "seckill:stock:"; // 对应的是商品随机码


    // TODO 此处有bug 如果有两个场次的信息并且各自对应不同的商品,获取远程调用结果的list集合就会报错 com.alibaba.fastjson.JSONException: TODO : 6
    //
    @Override
    public void uploadSeckillSkuLatest3Days() {
        Result result = couponFeignService.latest3DaySession();
        if (result.getRecode() == 200) {
            // 上架商品
            // TODO 此处有个神奇的地方是直接强制类型转换之后不会报错，但是拿去for循环的时候会报错类型转换异常 所以此处需要把对象转为json字符串然后再明确指定类型，转换为list集合
            // 在 foreach 循环中遇到 ClassCastException: java.util.LinkedHashMap cannot be cast to XXX 错误，通常是由于 JSON反序列化时未正确保留泛型类型信息，导致集合中的元素被解析为 LinkedHashMap 而非目标对象
            List<SeckillSessionWithSkus> resultList = JSONUtil.toList(JSONUtil.toJsonStr(result.getData()), SeckillSessionWithSkus.class);

            log.info("远程调用获取秒杀活动商品封装:{}", resultList);
            // 缓存到redis
            // 缓存活动信息
            saveSessionInfos(resultList);
            // 缓存活动的关联商品信息
            saveSessionSkuInfos(resultList);
        }
    }


    public void saveSessionInfos(List<SeckillSessionWithSkus> resultList) {
        resultList.stream().forEach(session -> {
            Long startTime = session.getStartTime().getTime();
            Long endTime = session.getEndTime().getTime();
            String key = SESSION_CACHE_PREFIX + startTime + "_" + endTime;
            // 获取三天内所有秒杀活动的商品id
            List<SeckillSkuVo> relationSkus = session.getRelationSkus();
            log.info("获取当前秒杀活动商品信息relationSkus:{}", JSON.toJSONString(relationSkus));
            List<String> seckillSkus = relationSkus.stream().map(item -> item.getPromotionSessionId() + "-" + item.getSkuId().toString()).toList();
            // 将秒杀活动商品集合缓存到redis
            Boolean hasKey = stringRedisTemplate.hasKey(key);
            if (!hasKey) {
                if (seckillSkus != null && !seckillSkus.isEmpty()) {
                    stringRedisTemplate.opsForList().leftPushAll(key, seckillSkus);
                    // 存到redis中就是 seckill:session:1744682400000_1744725600000
                }
            }

        });
        stringRedisTemplate.opsForValue().set("hello", "hello");
        log.info("秒杀活动信息缓存完毕......");
    }

    public void saveSessionSkuInfos(List<SeckillSessionWithSkus> resultList) {

        resultList.stream().forEach(session -> {
            // 准备hash操作
            BoundHashOperations<String, Object, Object> hashOps = stringRedisTemplate.boundHashOps(SKU_CACHE_PREFIX);
            session.getRelationSkus().stream().forEach(seckillSkuVo -> {
                log.info("准备缓存秒杀活动商品信息seckillSkuVo:{}", seckillSkuVo);
                // 缓存商品信息
                String redisKey = seckillSkuVo.getPromotionSessionId().toString() + "-" + seckillSkuVo.getSkuId().toString();
                if (!hashOps.hasKey(redisKey)) {
                    SeckillSkuRedisTo seckillSkuRedisTo = new SeckillSkuRedisTo();
                    // sku的基本数据
                    R skuResult = productFeignService.getSkuInfo(seckillSkuVo.getSkuId());
                    if (skuResult.getCode() == 0) {
                        SkuInfoVo skuInfo = skuResult.getData("skuInfo", new TypeReference<SkuInfoVo>() {
                        });
                        seckillSkuRedisTo.setSkuInfo(skuInfo);
                    }
                    // sku的秒杀信息
                    BeanUtil.copyProperties(seckillSkuVo, seckillSkuRedisTo);
                    // 设置当前商品的秒杀时间信息
                    seckillSkuRedisTo.setStartTime(session.getStartTime().getTime());
                    seckillSkuRedisTo.setEndTime(session.getEndTime().getTime());

                    // 随机码 请求中引入随机码防止得知商品id后脚本直接请求 相当于多一层验证码的作用
                    String token = RandomUtil.randomNumbers(6);
                    seckillSkuRedisTo.setRandomCode(token);

                    /**
                     * 这里的逻辑是先拿到秒杀活动的数量缓存到redis中作为信号(Semaphore) 然后根据信号量来限制到数据库的请求
                     * 引入分布式信号量    每一件商品都设置上自己的随机码 靠随机码来获取商品的库存信号量
                     */

                    String jsonString = JSON.toJSONString(seckillSkuRedisTo);
                    hashOps.put(redisKey, jsonString);

                    RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
                    semaphore.trySetPermits(seckillSkuVo.getSeckillCount());
                }

            });
        });
        log.info("商品信息缓存完毕......");
    }

    @Override
    public List<SeckillSkuRedisTo> getCurrentSeckillSku() {

        // 确定当前时间属于哪个秒杀场次
        long currentTime = System.currentTimeMillis();
        log.info("currentTime:{}", currentTime);

        // 获取这个秒杀场次需要的所有商品信息
        Set<String> keys = stringRedisTemplate.keys(SESSION_CACHE_PREFIX + "*");
        for (String key : keys) {
            String timeStr = key.replace(SESSION_CACHE_PREFIX, "");
            String[] times = timeStr.split("_");
            Long startTime = Long.parseLong(times[0]);
            Long endTime = Long.parseLong(times[1]);

            log.info("startTime{},endTime:{}", startTime, endTime);

            if (currentTime >= startTime && currentTime <= endTime) {
                log.info("查询到当前时间存在秒杀场次.....");
                List<String> range = stringRedisTemplate.opsForList().range(key, 0, -1);
                System.out.println("range = " + range);
                BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SKU_CACHE_PREFIX);
                List<String> skuInfoList = hashOps.multiGet(range);
                System.out.println("skuInfoList = " + skuInfoList);
                if (skuInfoList != null && skuInfoList.size() > 0) {
                    List<SeckillSkuRedisTo> seckillSkuRedisTos = skuInfoList.stream().map(item -> {

                        SeckillSkuRedisTo seckillSkuRedisTo = JSONUtil.toBean(item, SeckillSkuRedisTo.class);

                        // seckillSkuRedisTo.setRandomCode("");  // 查询当前时间秒杀活动的时候随机码可以保留
                        return seckillSkuRedisTo;
                    }).toList();
                    return seckillSkuRedisTos;
                }
                break;
            } else log.info("未查询到当前时间存在秒杀场次......");

        }
        return null;
    }


    @Override
    public SeckillSkuRedisTo getSkuSeckillInfo(Long skuId) {

        // redis中关于秒杀商品id的存储key都是场次id加上skuId(1_32,2_40),这里直接拿到所有的key分割后对比就能拿到秒杀商品信息
        BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SKU_CACHE_PREFIX);
        Set<String> keys = hashOps.keys();
        if (keys != null && keys.size() > 0) {
            for (String key : keys) {
                // 直接判断是否以指定商品id结尾
                if (key.endsWith(skuId.toString())) {
                    log.info("查询到redis中有该秒杀商品");
                    String skuInfoStr = hashOps.get(key);
                    SeckillSkuRedisTo skuRedisTo = JSONUtil.toBean(skuInfoStr, SeckillSkuRedisTo.class);
                    // 如果当前时间刚好在秒杀活动时间内就直接返回
                    long currentTimeMillis = System.currentTimeMillis();
                    if (currentTimeMillis >= skuRedisTo.getStartTime() && currentTimeMillis <= skuRedisTo.getEndTime()) {
                        log.info("查询到该秒杀商品正在秒杀活动时间内");
                        return skuRedisTo;
                    } else if (currentTimeMillis <= skuRedisTo.getStartTime()) {
                        // 如果当前时间还没开始秒杀,则返回数据时要去掉随机码
                        skuRedisTo.setRandomCode(null);
                        log.info("该秒杀商品尚未开始秒杀活动");
                        return skuRedisTo;
                    }
                    log.info("该秒杀商品活动已经过期");
                    // 如果活动已过期就直接返回null
                    return null;
                }
            }
        }
        log.info("未查询到该秒杀商品:{}", skuId);
        return null;
    }


    @Override
    public String kill(String killSkuId, String key, Integer num) {
        long s1 = System.currentTimeMillis();

        // 获取用户唯一标识 买过就不能再买了 保证幂等性
        MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();

        // 合法性校验 获取redis中当前秒杀商品的详细信息 在里面是以3-11这种格式存储的
        BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SKU_CACHE_PREFIX);
        String seckillSkuRedisToJson = hashOps.get(killSkuId);
        if (StrUtil.isBlank(seckillSkuRedisToJson)) {
            return null;
        } else {
            SeckillSkuRedisTo skuRedisTo = JSONUtil.toBean(seckillSkuRedisToJson, SeckillSkuRedisTo.class);
            System.out.println("skuRedisTo = " + skuRedisTo);
            // 校验秒杀时间是否合法
            Long startTime = skuRedisTo.getStartTime();
            Long endTime = skuRedisTo.getEndTime();
            Long thisTime = new Date().getTime();
            Long ttl = endTime - thisTime;
            if (thisTime >= startTime && thisTime <= endTime) {
                // 时间合法之后 继续校验随机码
                String redisKey = skuRedisTo.getPromotionSessionId() + "-" + skuRedisTo.getSkuId();
                String randomCode = skuRedisTo.getRandomCode();
                if (randomCode.equals(key) && killSkuId.equals(redisKey)) {
                    // 验证购物数量是否超过限购 没超过限购就不占位
                    //TODO 这里有个bug但我懒得写了,直接补充上得了 用户只能买一次不超过限购数量的商品,买完就不能买了
                    // 限购2 买1 之后想再买1也不行了因为已经占位了 解决该bug需要在数据库中再存个当前限购商品已经购买的数量就行
                    // 如果这次秒杀请求的数量+已买数量=限购数量然后再进行占位 否则不占位,只存商品数量
                    if (num <= skuRedisTo.getSeckillLimit()) {
                        // 验证这个人是否已经买过了  秒杀成功就需要去redis占位 保证幂等性 这里的key值设置为userid_场次_商品id 过期时间就是商品秒杀时间
                        String userPlaceholder = memberResponseVo.getId() + "_" + redisKey;
                        // 自动过期
                        Boolean ifAbsent = stringRedisTemplate.opsForValue().setIfAbsent(userPlaceholder, num.toString(), ttl, TimeUnit.MILLISECONDS);
                        if (ifAbsent) {
                            // 占位成功说明之前没买过
                            RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);
                            boolean isSuccess = semaphore.tryAcquire(num);
                            if (isSuccess) {
                                // 秒杀成功 返回一个订单号 发送mq消息到order服务
                                String orderToken = RandomUtil.randomNumbers(6);

                                SeckillOrderTo seckillOrderTo = new SeckillOrderTo();
                                seckillOrderTo.setOrderSn(orderToken);
                                seckillOrderTo.setMemberId(memberResponseVo.getId());
                                seckillOrderTo.setNum(num);
                                seckillOrderTo.setPromotionSessionId(skuRedisTo.getPromotionId());
                                seckillOrderTo.setSkuId(skuRedisTo.getSkuId());
                                seckillOrderTo.setSeckillPrice(skuRedisTo.getSeckillPrice());

                                rabbitTemplate.convertAndSend("order-event-exchange", "order.seckill.order", seckillOrderTo);
                                long s2 = System.currentTimeMillis();
                                log.info("秒杀信息已推送到消息队列:{},共计耗时:{}毫秒", seckillOrderTo, s2 - s1);
                                return orderToken;
                            }
                        } else return null;
                    }

                } else return null;
            } else return null;
        }
        return null;
    }

}
